﻿package org.papervision3d.objects;

import nme.display.BlendMode;
import nme.filters.BitmapFilter;

import org.papervision3d.Papervision3D;
import org.papervision3d.core.culling.FrustumTestMethod;
import org.papervision3d.core.data.UserData;
import org.papervision3d.core.geom.renderables.Triangle3D;
import org.papervision3d.core.log.PaperLogger;
import org.papervision3d.core.material.AbstractLightShadeMaterial;
import org.papervision3d.core.math.Matrix3D;
import org.papervision3d.core.math.Number3D;
import org.papervision3d.core.math.Quaternion;
import org.papervision3d.core.proto.CameraObject3D;
import org.papervision3d.core.proto.DisplayObjectContainer3D;
import org.papervision3d.core.proto.GeometryObject3D;
import org.papervision3d.core.proto.MaterialObject3D;
import org.papervision3d.core.proto.SceneObject3D;
import org.papervision3d.core.render.data.RenderSessionData;
import org.papervision3d.materials.shaders.ShadedMaterial;
import org.papervision3d.materials.utils.MaterialsList;
import org.papervision3d.view.Viewport3D;
import org.papervision3d.view.layer.ViewportLayer;

/**
 * The DisplayObject class represents instances of 3D objects that are contained in the scene.
* <p/>
* That includes all objects in the scene, not only those that can be rendered, but also the camera and its target.
* <p/>
* The DisplayObject3D class supports basic functionality like the x, y and z position of an object, as well as rotationX, rotationY, rotationZ, scaleX, scaleY and scaleZ and visible. It also supports more advanced properties of the object such as its transform Matrix3D.
* <p/>
* <p/>
* DisplayObject3D is not an abstract base class; therefore, you can call DisplayObject3D directly. Invoking new DisplayObject() creates a new empty object in 3D space, like when you used createEmptyMovieClip().
*
*/
class DisplayObject3D extends DisplayObjectContainer3D
{
   /**
	* tells Mesh3D's render() method to sort by measuring from the center of a triangle
	*/
	public static inline var MESH_SORT_CENTER:Int = 1;
	
	/**
	* tells Mesh3D's render() method to sort by measuring from the farthest point of a triangle
	*/
	public static inline var MESH_SORT_FAR:Int = 2;
	
	/**
	* tells Mesh3D's render() method to sort by measuring from the closest point of a triangle
	*/
	public static inline var MESH_SORT_CLOSE:Int = 3;
	
/**
	* A Matrix3D object containing values that affect the scaling, rotation, and translation of the display object.
	*/
	public var transform :Matrix3D;

	/**
	* [internal-use] A camera transformed Matrix3D object.
	*/
	public var view      :Matrix3D;

	/**
	* World transformation.
	*/
	public var world     :Matrix3D;

	/**
	* [internal-use]
	*/
	public var faces     :Array<BitmapFilter>;

	/**
	 * This allows objects faces to have their own containers.
	 */
	public static var faceLevelMode  :Bool;
	
	public static var sortedArray:Array<Dynamic> = [];
	
	/**
	* The GeometryObject3D object that contains the 3D definition of this instance.
	* <p/>
	* When different objects share the same geometry, they become instances. They are the same object, displayed multiple times. Changing the shape of this object changes the shape of all of its instances.
	* <p/>
	* Instancing an object saves system memory, and is useful to display an object multiple times while maintaining its shape.
	* <p/>
	* For example, you could create armies and forests full of duplicate objects without needing the memory to handle that much actual geometry. Each instance has its own transform node so it can have its own position, rotation, and scaling.
	*/
	public var geometry :GeometryObject3D;

	/**
	* [internal-use] The average depth of the object faces center. Used internally for z-sorting.
	*/
	public var screenZ :Float;
	
	/**
	* [internal use] Is this object culled by camera frustum?
	*/
	public var culled:Bool;
	
	/**
	* The list of materials for this instance.
	*/
	public var materials   :MaterialsList;
	
	/**
	* tells Mesh3D's render() method to compare the measurement choice of the user for a triangle's sorting
	*/
	public var meshSort:Int;
	
	public var container			:ViewportLayer;	
	public var alpha				:Float;
	public var blendMode			:BlendMode;
	public var filters				:Array<BitmapFilter>;
	public var parentContainer		:DisplayObject3D;
	public var flipLightDirection	:Bool;
	public var frustumTestMethod	:Int;
	
	/**
	* [read-only] Indicates the DisplayObjectContainer3D object that contains this display object.
	*/
	public var parent :DisplayObjectContainer3D;
	
	/**
	* [read-only] The coordinate of the object on screen.
	*/
	public var screen :Number3D;

	/**
	* Whether or not the display object is visible.
	* <p/>
	* A Boolean value that indicates whether the object is projected, transformed and rendered. A value of false will effectively ignore the object. The default value is true.
	*/
	public var visible :Bool;

	/**
	* An optional object name.
	*/
	public var name :String;

	/**
	* [read-only] Unique id of this instance.
	*/
	public var id :Int;

	/**
	* An object that contains user defined properties.
	* <p/>
	* All properties of the extra field are copied into the new instance. The properties specified with extra are publicly available.
	*/
	public var extra :Dynamic; // = {}; TBD
	
	
	public var cullTest:Float;
	public var useClipping:Bool;
	
	public var testQuad:Bool;
			
	/**
	* Relative directions.
	*/
	static private inline var FORWARD  	:Number3D = new Number3D(  0,  0,  1 );
	static private inline var BACKWARD 	:Number3D = new Number3D(  0,  0, -1 );
	static private inline var LEFT     	:Number3D = new Number3D( -1,  0,  0 );
	static private inline var RIGHT    	:Number3D = new Number3D(  1,  0,  0 );
	static private inline var UP       	:Number3D = new Number3D(  0,  1,  0 );
	static private inline var DOWN     	:Number3D = new Number3D(  0, -1,  0 );
	
	private static var _tempMatrix	:Matrix3D = Matrix3D.IDENTITY; 
	private static var _tempQuat	:Quaternion = new Quaternion(); 
	private static var _newID		:Int = 0;
	private static var toDEGREES 	:Float = 180/Math.PI;
	private static var toRADIANS 	:Float = Math.PI/180;
	private static var entry_count	:Int = 0;
	
	/**
	* [internal-use]
	*/
	private var _transformDirty 	:Bool= false;
	private var _sorted       	:Array<Dynamic>;
	private var _useOwnContainer	:Bool= false;
	private var _userData			:UserData;
	
	/**
	* The scene where the object belongs.
	*/
	private var _scene 			:SceneObject3D = null;
		
	/**
	 * pre-made Number3Ds and Matrix3Ds for use in the lookAt function
	 * and others
	 * 
	 */
	private	var _position 			:Number3D;
	private	var _lookatTarget   	:Number3D;
	private	var _zAxis 				:Number3D;
	private	var _xAxis 				:Number3D;
	private	var _yAxis 				:Number3D;
	private var _rotation			:Number3D; 
	private var _rotationDirty  	:Bool;
	private var _rotationX      	:Float;
	private var _rotationY     	 	:Float;
	private var _rotationZ      	:Float;
	private var _scaleX         	:Float;
	private var _scaleY         	:Float;
	private var _scaleZ         	:Float;
	private var _scaleDirty     	:Bool;
	private var _tempScale			:Number3D;
	private var _numClones			:Int;
	private var _material			:MaterialObject3D;
	private var _rot				:Quaternion;
	
	private var _qPitch		:Quaternion;
	private var _qYaw		:Quaternion;
	private var _qRoll		:Quaternion;
	
	private var _localRotationX	:Float;
	private var _localRotationY	:Float;
	private var _localRotationZ	:Float;
	
	private var _autoCalcScreenCoords:Bool;
	
	/**
	* Returns an empty DiplayObject3D object positioned in the center of the 3D coordinate system (0, 0 ,0).
	*/
	public static var ZERO(get_ZERO, null):DisplayObject3D;
	static private function get_ZERO():DisplayObject3D
	{
		return new DisplayObject3D();
	}
	
	public var useOwnContainer(get_useOwnContainer, set_useOwnContainer):Bool;
	private function set_useOwnContainer(value:Bool):Bool
	{
		_useOwnContainer = value;
		setParentContainer(this, true);
		return _useOwnContainer;
	}
	
	private function get_useOwnContainer():Bool
	{
		return _useOwnContainer;
	}
	
	public var userData(get_userData, set_userData):UserData;
	private function set_userData(userData:UserData):UserData
	{
		_userData = userData;
		return _userData;
	}
	
	private function get_userData():UserData
	{
		return _userData;	
	}
	
	/**
	* An Number that sets the X coordinate of a object relative to the origin of its parent.
	*/
	public var x(get_x, set_x):Float;
	private function get_x():Float
	{
		return this.transform.n14;
	}

	private function set_x( value:Float ):Float
	{
		this.transform.n14 = value;
		return value;
	}
	
	/**
	* An Number that sets the Y coordinate of a object relative to the origin of its parent.
	*/
	public var y(get_y, set_y):Float;
	private function get_y():Float
	{
		return this.transform.n24;
	}

	private function set_y( value:Float ):Float
	{
		this.transform.n24 = value;
		return value;
	}

	/**
	* An Number that sets the Z coordinate of a object relative to the origin of its parent.
	*/
	public var z(get_z, set_z):Float;
	private function get_z():Float
	{
		return this.transform.n34;
	}

	private function set_z( value:Float ):Float
	{
		this.transform.n34 = value;
		return value;
	}
	
	/**
	 * A Number3D containing the current x, y, and z of the DisplayObject3D
	 */
	 public var position(get_position, set_position):Number3D;
	 private function get_position():Number3D
	 {
		_position.reset(this.x, this.y, this.z);
		return _position;
	}

	private function set_position(n:Number3D):Number3D
	 {
		this.x = n.x;
		this.y = n.y;
		this.z = n.z;
		return n;
	 }
	 
	/**
	* Specifies the rotation around the X axis from its original orientation.
	*/
	public var rotationX(get_rotationX, set_rotationX):Float;
	private function get_rotationX():Float
	{
		if( this._rotationDirty ) updateRotation();

		return Papervision3D.useDEGREES ? this._rotationX * toDEGREES : this._rotationX;
	}

	private function set_rotationX( rot:Float ):Float
	{
		this._rotationX = Papervision3D.useDEGREES ? rot * toRADIANS : rot;

		this._transformDirty = true;
		
		return rot;
	}

	/**
	* Specifies the rotation around the Y axis from its original orientation.
	*/
	public var rotationY(get_rotationY, set_rotationY):Float;
	private function get_rotationY():Float
	{
		if( this._rotationDirty ) updateRotation();

		return Papervision3D.useDEGREES ? this._rotationY * toDEGREES : this._rotationY;
	}

	private function set_rotationY( rot:Float ):Float
	{
		this._rotationY = Papervision3D.useDEGREES ? rot * toRADIANS : rot;
	
		this._transformDirty = true;
		
		return rot;
	}

	/**
	* Specifies the rotation around the Z axis from its original orientation.
	*/
	public var rotationZ(get_rotationZ, set_rotationZ):Float;
	private function get_rotationZ():Float
	{
		if( this._rotationDirty ) updateRotation();

		return Papervision3D.useDEGREES ? this._rotationZ * toDEGREES : this._rotationZ;
	}

	private function set_rotationZ( rot:Float ):Float
	{
		this._rotationZ = Papervision3D.useDEGREES ? rot * toRADIANS : rot;
		
		this._transformDirty = true;
		
		return rot;
	}

	// ___________________________________________________________________ S C A L E

	/**
	* Sets the 3D scale as applied from the registration point of the object.
	*/
	public var scale(get_scale, set_scale):Float;
	private function get_scale():Float
	{
		if( this._scaleX == this._scaleY && this._scaleX == this._scaleZ )
			if( Papervision3D.usePERCENT ) return this._scaleX * 100;
			else return this._scaleX;
		else return Math.NaN;
	}

	private function set_scale( scale:Float ):Float
	{
		if( this._rotationDirty ) updateRotation();
		
		
		if( Papervision3D.usePERCENT ) scale /= 100;

		this._scaleX = this._scaleY = this._scaleZ = scale;

		this._transformDirty = true;
		
		return scale;
	}


	/**
	* Sets the scale along the local X axis as applied from the registration point of the object.
	*/
	public var scaleX(get_scaleX, set_scaleX):Float;
	private function get_scaleX():Float
	{
		if( Papervision3D.usePERCENT ) return this._scaleX * 100;
		else return this._scaleX;
	}

	private function set_scaleX( scale:Float ):Float
	{
		if( this._rotationDirty ) updateRotation();
		
		if( Papervision3D.usePERCENT ) this._scaleX = scale / 100;
		else this._scaleX = scale;

		this._transformDirty = true;
		
		return scale;
	}

	/**
	* Sets the scale along the local Y axis as applied from the registration point of the object.
	*/
	public var scaleY(get_scaleY, set_scaleY):Float;
	private function get_scaleY():Float
	{
		if( Papervision3D.usePERCENT ) return this._scaleY * 100;
		else return this._scaleY;
	}

	private function set_scaleY( scale:Float ):Float
	{
		if( this._rotationDirty ) updateRotation();
		
		if( Papervision3D.usePERCENT ) this._scaleY = scale / 100;
		else this._scaleY = scale;

		this._transformDirty = true;
		
		return scale;
	}

	/**
	* Sets the scale along the local Z axis as applied from the registration point of the object.
	*/
	public var scaleZ(get_scaleZ, set_scaleZ):Float;
	private function get_scaleZ():Float
	{
		if( Papervision3D.usePERCENT ) return this._scaleZ * 100;
		else return this._scaleZ;
	}

	private function set_scaleZ( scale:Float ):Float
	{
		if( this._rotationDirty ) updateRotation();
		
		if( Papervision3D.usePERCENT ) this._scaleZ = scale / 100;
		else this._scaleZ = scale;

		this._transformDirty = true;
		
		return scale;
	}

	
	

	/**
	* The X coordinate of a object relative to the scene coordinate system.
	*/
	public var sceneX(get_sceneX, null):Float;
	private function get_sceneX():Float
	{
		return this.world.n14;
	}

	/**
	* The Y coordinate of a object relative to the scene coordinate system.
	*/
	public var sceneY(get_sceneY, null):Float;
	private function get_sceneY():Float
	{
		return this.world.n24;
	}

	/**
	* The Z coordinate of a object relative to the scene coordinate system.
	*/
	public var sceneZ(get_sceneZ, null):Float;
	private function get_sceneZ():Float
	{
		return this.world.n34;
	}

	/**
	* The default material for the object instance. Materials collect data about how objects appear when rendered.
	*/
	public var material(get_material, set_material):MaterialObject3D;
	private function set_material(material:MaterialObject3D):MaterialObject3D
	{
		if(_material != null){
			_material.unregisterObject(this);
		}
		_material = material;
		if (_material != null){
			_material.registerObject(this);
		}
		
		return _material;
	}
	
	private function get_material():MaterialObject3D
	{
		return _material;
	}
	
	public var scene(get_scene, set_scene):SceneObject3D; 
	private function set_scene(p_scene:SceneObject3D):SceneObject3D
	{
		// set scene property
		_scene = p_scene;
		
		for(child in this._childrenByName )
		{
			if(child.scene == null) child.scene = _scene;
		}
		
		return p_scene;
	}
	
	private function get_scene():SceneObject3D
	{
		return _scene;
	}
	
	public var autoCalcScreenCoords(get_autoCalcScreenCoords, set_autoCalcScreenCoords):Bool;
	private function set_autoCalcScreenCoords(autoCalculateScreenCoords:Bool):Bool
	{
		_autoCalcScreenCoords = autoCalculateScreenCoords;
		
		return autoCalculateScreenCoords;
	}
	
	private function get_autoCalcScreenCoords():Bool
	{
		return _autoCalcScreenCoords ;
	}

	/**
	* Creates a new DisplayObject3D instance. After creating the instance, call the addChild() method of a DisplayObjectContainer3D.
	*
	* @param	name		[optional] - The name of the newly created object.
	* @param	geometry	[optional] - The geometry of the newly created object.
	*
	* <ul>
	* <li><b>x</b></b>: An Number that sets the X coordinate of a object relative to the scene coordinate system.</li>
	* <p/>
	* <li><b>y</b>: An Number that sets the Y coordinate of a object relative to the scene coordinate system.</li>
	* <p/>
	* <li><b>z</b>: An Number that sets the Z coordinate of a object relative to the scene coordinate system.</li>
	* <p/>
	* <li><b>rotationX</b>: Specifies the rotation around the X axis from its original orientation.</li>
	* <p/>
	* <li><b>rotationY</b>: Specifies the rotation around the Y axis from its original orientation.</li>
	* <p/>
	* <li><b>rotationZ</b>: Specifies the rotation around the Z axis from its original orientation.</li>
	* <p/>
	* <li><b>scaleX</b>: Sets the scale along the local X axis as applied from the registration point of the object.</li>
	* <p/>
	* <li><b>scaleY</b>: Sets the scale along the local Y axis as applied from the registration point of the object.</li>
	* <p/>
	* <li><b>scaleZ</b>: Sets the scale along the local Z axis as applied from the registration point of the object.</li>
	* <p/>
	* <li><b>visible</b>: Whether or not the display object is visible.
	* <p/>
	* A Boolean value that indicates whether the object is projected, transformed and rendered. A value of false will effectively ignore the object. The default value is true.</li>
	* <p/>
	* <li><b>container</b>: The MovieClip that you draw into when rendering. Use only when the object is rendered in its own unique MovieClip.
	* <p/>
	* It's Boolean value determines whether the container MovieClip should be cleared before rendering.</li>
	* <p/>
	* <li><b>extra</b>: An object that contains user defined properties.
	* <p/>
	* All properties of the extra field are copied into the new instance. The properties specified with extra are publicly available.</li>
	* </ul>
	*/
	public function new( name:String=null, geometry:GeometryObject3D=null ):Void
	{
		faces  = [];
		screen = new Number3D();

		meshSort = MESH_SORT_CENTER;
		alpha				 = 1;
		blendMode			 = BlendMode.NORMAL;
		filters				 = [];
		flipLightDirection = false;
		frustumTestMethod	 = FrustumTestMethod.BOUNDING_SPHERE;
		cullTest = 0;
		useClipping = true;
		testQuad = true;
		_transformDirty = false;
		_position = Number3D.ZERO;
		_lookatTarget = Number3D.ZERO;
		_zAxis = Number3D.ZERO;
		_xAxis = Number3D.ZERO;
		_yAxis = Number3D.ZERO;
		_rotation = Number3D.ZERO; 
		_rotationDirty = false;
		_scaleDirty = false;
		_tempScale;
		_numClones = 0;
		_rot				 = new Quaternion();

		_qPitch		 = new Quaternion();
		_qYaw		 = new Quaternion();
		_qRoll		 = new Quaternion();
		
		_localRotationX	 = 0;
		_localRotationY	 = 0;
		_localRotationZ	 = 0;
		
		_autoCalcScreenCoords = false;
		
		
		
		super();
		
		if(name!=null){
			PaperLogger.info( "DisplayObject3D: " + name );
		}
		this.culled    = false;
		
		this.transform = Matrix3D.IDENTITY;
		this.world     = Matrix3D.IDENTITY;
		this.view      = Matrix3D.IDENTITY;

		this.x =  0;
		this.y =  0;
		this.z =  0;
		
		rotationX = 0;
		rotationY = 0;
		rotationZ = 0;

		_localRotationX = _localRotationY = _localRotationZ = 0;
		
		var scaleDefault:Float = Papervision3D.usePERCENT? 100 : 1;
		scaleX = scaleDefault;
		scaleY = scaleDefault;
		scaleZ = scaleDefault;
		_tempScale = new Number3D();
		
		this.visible = true;

		this.id = _newID++;
		this.name = name != null? name : Std.string( this.id );

		_numClones = 0;
		
		if( geometry != null) addGeometry( geometry );
	}

	
	/**
	* Adds a child DisplayObject3D instance to this DisplayObjectContainer instance.
	*
	* [TODO: If you add a child object that already has a different display object container as a parent, the object is removed from the child list of the other display object container.]
	*
	* @param	child	The DisplayObject3D instance to add as a child of this DisplayObjectContainer3D instance.
	* @param	name	An optional name of the child to add or create. If no name is provided, the child name will be used.
	* @return	The DisplayObject3D instance that you have added or created.
	*/
	public override function addChild( child :DisplayObject3D, name:String=null ):DisplayObject3D
	{
		child = super.addChild( child, name );
		
		if( child.scene == null ) child.scene = scene;
		if( this.useOwnContainer ){
				child.parentContainer = this;
		}
		return child;
	}
	
	/**
	* Adds a geometry definition to the instance.
	*
	* A geometry describes the visual shape and appearance of an object in a scene.
	*
	* @param	geometry	A geometry definition.
	*/
	public function addGeometry( geometry:GeometryObject3D=null ):Void
	{
		if( geometry != null)
			this.geometry = geometry;	
	}
	
	/**
	 * Clones this object.
	 * 
	 * @return	The cloned DisplayObject3D.
	 */ 
	public function clone():DisplayObject3D
	{
		var name:String = this.name + "_" + (_numClones++);
		var object:DisplayObject3D = new DisplayObject3D(name);
		//var object:DisplayObject3D = new DisplayObject3D();

		if(this.material != null)
			object.material = this.material;
		
		if(this.materials != null)
			object.materials = this.materials.clone();
			
		if(this.geometry != null)
		{
			object.geometry = this.geometry.clone(object);
			object.geometry.ready = true;
		}
		
		object.copyTransform(this);
		
		for(child in this.children)
			object.addChild(child.clone());
			
		return object;
	}
	

	/**
	* Gets the distance to the position of the given object.
	*
	* @param	obj		The display object to measure the distance to.
	* @return	The distance to the registration point of the given object.
	*/
	public function distanceTo( obj:DisplayObject3D ):Float
	{
		var x :Float = this.x - obj.x;
		var y :Float = this.y - obj.y;
		var z :Float = this.z - obj.z;

		return Math.sqrt( x*x + y*y + z*z );
	}


	/**
	* Evaluates the display object to see if it overlaps or intersects with the point specified by the x, y and z parameters.
	* <p/>
	* The x, y and z parameters specify a point in the coordinate space of the instance parent object, not the scene (unless that parent object is the scene).
	*
	* @param	x	The x coordinate to test against this object.
	* @param	y	The y coordinate to test against this object.
	* @param	z	The z coordinate to test against this object.
	* @return	true if the display object overlaps or intersects with the specified point; false otherwise.
	*/
	public function hitTestPoint( x:Float, y:Float, z:Float ):Bool
	{
		var dx :Float = this.x - x;
		var dy :Float = this.y - y;
		var dz :Float = this.z - z;

		var d2 :Float = dx*dx + dy*dy + dz*dz;

		var sA :Float = this.geometry != null? this.geometry.boundingSphere.maxDistance : 0;

		return sA > d2;
	}


	/**
	* Evaluates the display object to see if it overlaps or intersects with the obj display object.
	*
	* @param	obj	 The display object to test against.
	* @return	true if the display objects intersect; false if not.
	*/
	// TODO: Use group boundingSphere
	public function hitTestObject( obj:DisplayObject3D, multiplier:Float=1 ):Bool
	{
		var dx :Float = this.x - obj.x;
		var dy :Float = this.y - obj.y;
		var dz :Float = this.z - obj.z;

		var d2 :Float = dx*dx + dy*dy + dz*dz;

		var sA :Float = this.geometry != null? this.geometry.boundingSphere.maxDistance : 0;
		var sB :Float = obj.geometry != null?  obj.geometry.boundingSphere.maxDistance  : 0;
		
		sA = sA * multiplier;

		return sA + sB > d2;
	}

	// ___________________________________________________________________________________________________
	//                                                                                   M A T E R I A L S

	/**
	* Returns the material that exists with the specified name in the materials list.
	* </p>
	* If more that one material object has the specified name, the method returns the first material object in the materials list.
	* </p>
	* @param	name	The name of the material to return.
	* @return	The material object with the specified name.
	*/
	// TODO: Recursive
	public function getMaterialByName( name:String ):MaterialObject3D
	{
		var material:MaterialObject3D = this.materials != null ? this.materials.getMaterialByName( name ) : null;
		if( material != null)
		{
			return material;
		}
		else
		{
			for( child  in this._childrenByName )
			{
				material = child.getMaterialByName( name );
				if( material != null) return material;
			}
		}
		return null;
	}

	/**
	* Returns a string value with the list of material names of the materials list.
	*
	* @return	A string.
	*/
	// TODO: Recursive
	public function materialsList():String
	{
		var list:String = "";

		for( name in Reflect.fields(this.materials) )
			list += name + "\n";

		for( child in this._childrenByName )
		{
			for( name in child.materials.materialsByName )
				list += "+ " + name + "\n";
		}
		
		return list;
	}
	
	
	/**
	 * Replaces a material by its name.
	 * 
	 * @param	material
	 * @param	name
	 * @return
	 */
	public function replaceMaterialByName(material:MaterialObject3D, name:String):Void
	{
		if(this.materials == null)
			return;
		
		var existingMaterial:MaterialObject3D = this.materials.getMaterialByName(name);
		if(existingMaterial == null)
			return;
			
		if(this.material == existingMaterial)
			this.material = material;
		existingMaterial = this.materials.removeMaterial(existingMaterial);

		material = this.materials.addMaterial(material, name);
			
		updateMaterials(this, existingMaterial, material);
	}
	
	/**
	 * Sets the material for a child DisplayObject3D.
	 * 
	 * @param child		A child DisplayObject3D of this DAE.
	 * @param material	The new material for the child.
	 * @param existingMaterial Optional existing material to be replaced.
	 */
	public function setChildMaterial(child:DisplayObject3D, material:MaterialObject3D, existingMaterial:MaterialObject3D=null):Void 
	{	
		if(child == null) 
			return;	
		
		if(existingMaterial == null || child.material == existingMaterial)
			child.material = material;
			
		if(child.geometry != null && child.geometry.faces != null)
		{
			for( triangle in child.geometry.faces )
			{
				if(existingMaterial == null || triangle.material == existingMaterial)
					triangle.material = material;
			}
		}
		
	}
	
	/**
	 * Sets the material for a child DisplayObject3D by the child's name.
	 * 
	 * @param childName The name of the DisplayObject3D.
	 * @param material	The new material for the child.
	 */
	public function setChildMaterialByName(childName:String, material:MaterialObject3D):Void 
	{
		setChildMaterial(getChildByName(childName, true), material);
	}
	
	
	
	/**
	 * 
	 * @param	do3d
	 * @param	existingMaterial
	 * @param	newMaterial
	 */
	private function updateMaterials(do3d:DisplayObject3D, existingMaterial:MaterialObject3D, newMaterial:MaterialObject3D):Void
	{
		existingMaterial.unregisterObject(do3d);
		
		// register shaded materials with its object
		if(Std.is(newMaterial, AbstractLightShadeMaterial) || Std.is(newMaterial, ShadedMaterial))
		{
			newMaterial.registerObject(do3d);
		}
				
		if( do3d.material == existingMaterial )
			do3d.material = newMaterial;
				
		if( do3d.geometry != null && do3d.geometry.faces != null && do3d.geometry.faces.length != 0 )
		{
			for( triangle in do3d.geometry.faces )
			{
				if( triangle.material == existingMaterial )
					triangle.material = newMaterial;
			}
		}
		
		for(child in do3d.children)
			updateMaterials(child, existingMaterial, newMaterial);
	}
	
	
	

	/**
	* [internal-use] Projects three dimensional coordinates onto a two dimensional plane to simulate the relationship of the camera to subject.
	* <p/>
	* This is the first step in the process of representing three dimensional shapes two dimensionally.
	*
	* @param	parent	The DisplayObject3D object that contains this display object.
	* @param	renderSessionData Data for the current render.
	*/
	public function project( parent :DisplayObject3D, renderSessionData:RenderSessionData):Float
	{	
		if( this._transformDirty ) updateTransform();
		
		this.world.calculateMultiply( parent.world, this.transform );
		
		if( renderSessionData.camera.culler  != null )
		{ 
			if( this == renderSessionData.camera )
				this.culled = true;
			else
				this.culled = (renderSessionData.camera.culler.testObject(this) < 0);
			if( this.culled ){
				renderSessionData.renderStatistics.culledObjects ++ ;
				return 0;
			} 			
		}
		else
		{
			this.culled = false;
		}
		
		if( parent != renderSessionData.camera )
		{
			if(renderSessionData.camera.useProjectionMatrix)
				this.view.calculateMultiply4x4( parent.view, this.transform );
			else
				this.view.calculateMultiply( parent.view, this.transform );
		}
		else
		{
			if(renderSessionData.camera.useProjectionMatrix)
				this.view.calculateMultiply4x4( renderSessionData.camera.eye, this.transform );
			else
				this.view.calculateMultiply( renderSessionData.camera.eye, this.transform );
		}
		
		if(_autoCalcScreenCoords){
			calculateScreenCoords(renderSessionData.camera);
		}
		
		var screenZs :Float = 0;
		var children :Float = 0;
		
		for( child in this._childrenByName )
		{
			if( child.visible )
			{
				screenZs += child.project( this, renderSessionData );
				children++;
			}
		}

		return this.screenZ = screenZs / children;
	}
	
	
	
	/**
	 * 
	 */
	public function calculateScreenCoords( camera :CameraObject3D ):Void
	{
		if(camera.useProjectionMatrix)
		{
			var vx	:Float = 0;
			var vy	:Float = 0;
			var vz	:Float = 0;
			var w	:Float = vx * view.n41 + vy * view.n42 + vz * view.n43 + view.n44;
			var vpw	:Float = camera.viewport.width / 2;
			var vph	:Float = camera.viewport.height / 2;
			
			screen.x = (vx * view.n11 + vy * view.n12 + vz * view.n13 + view.n14) / w;
			screen.y = (vx * view.n21 + vy * view.n22 + vz * view.n23 + view.n24) / w;
			screen.z = vx * view.n31 + vy * view.n32 + vz * view.n33 + view.n34;
			
			screen.x *= vpw;
			screen.y *= vph;
		}
		else
		{
			var persp:Float = (camera.focus * camera.zoom) / (camera.focus + view.n34);
			screen.x = view.n14 * persp;
			screen.y = view.n24 * persp;
			screen.z = view.n34;
		}
	}

	/**
	* Translate the display object in the direction it is facing, i.e. it's positive Z axis.
	*
	* @param	distance	The distance that the object should move forward.
	*/
	public function moveForward  ( distance:Float ):Void { translate( distance, FORWARD  ); }

	/**
	* Translate the display object in the opposite direction it is facing, i.e. it's negative Z axis.
	*
	* @param	distance	The distance that the object should move backward.
	*/
	public function moveBackward ( distance:Float ):Void { translate( distance, BACKWARD ); }

	/**
	* Translate the display object lateraly, to the left of the direction it is facing, i.e. it's negative X axis.
	*
	* @param	distance	The distance that the object should move left.
	*/
	public function moveLeft     ( distance:Float ):Void { translate( distance, LEFT     ); }

	/**
	* Translate the display object lateraly, to the right of the direction it is facing, i.e. it's positive X axis.
	*
	* @param	distance	The distance that the object should move right.
	*/
	public function moveRight    ( distance:Float ):Void { translate( distance, RIGHT    ); }

	/**
	* Translate the display object upwards, with respect to the direction it is facing, i.e. it's positive Y axis.
	*
	* @param	distance	The distance that the object should move up.
	*/
	public function moveUp       ( distance:Float ):Void { translate( distance, UP       ); }

	/**
	* Translate the display object downwards, with respect to the direction it is facing, i.e. it's negative Y axis.
	*
	* @param	distance	The distance that the object should move down.
	*/
	public function moveDown     ( distance:Float ):Void { translate( distance, DOWN     ); }

	// ___________________________________________________________________________________________________
	//                                                                   L O C A L   T R A N S L A T I O N

	/**
	* Move the object along a given direction.
	*
	* @param	distance	The distance that the object should travel.
	* @param	axis		The direction that the object should move towards.
	*/
	public function translate( distance:Float, axis:Number3D ):Void
	{
		var vector:Number3D = axis.clone();

		if( this._transformDirty ) updateTransform();

		Matrix3D.rotateAxis( transform, vector );

		this.x += distance * vector.x;
		this.y += distance * vector.y;
		this.z += distance * vector.z;
	}

	// ___________________________________________________________________________________________________
	//                                                                         L O C A L   R O T A T I O N
	
	/**
	* Rotate the display object around its lateral or transverse axis —an axis running from the pilot's left to right in piloted aircraft, and parallel to the wings of a winged aircraft; thus the nose pitches up and the tail down, or vice-versa.
	*
	* @param	angle	The angle to rotate.
	*/
	public var localRotationX(get_localRotationX, set_localRotationX):Float;
	private function set_localRotationX( angle:Float ):Float
	{
		angle = Papervision3D.useDEGREES? angle * toRADIANS : angle;

		if( this._transformDirty ) updateTransform();

		_qPitch.setFromAxisAngle(transform.n11, transform.n21, transform.n31, _localRotationX-angle );
		
		this.transform.calculateMultiply3x3( _qPitch.matrix , transform );
		
		_localRotationX = angle;
		
		_rotationDirty = true;
		
		return angle;
	}
	
	private function get_localRotationX():Float
	{
		return Papervision3D.useDEGREES ? _localRotationX * toDEGREES : _localRotationX;
	}
	
	/**
	* Rotate the display object around about the vertical axis —an axis drawn from top to bottom.
	*
	* @param	angle	The angle to rotate.
	*/
	public var localRotationY(get_localRotationY, set_localRotationY):Float;
	private function set_localRotationY( angle:Float ):Float
	{
		angle = Papervision3D.useDEGREES? angle * toRADIANS : angle;

		if( _transformDirty ) updateTransform();

		_qYaw.setFromAxisAngle( transform.n12, transform.n22, transform.n32, _localRotationY-angle );

		transform.calculateMultiply3x3( _qYaw.matrix, transform );
		
		_localRotationY = angle;
		
		_rotationDirty = true;
		
		return angle;
	}
	
	private function get_localRotationY():Float
	{
		return Papervision3D.useDEGREES ? _localRotationY * toDEGREES : _localRotationY;
	}
	
	/**
	* Rotate the display object around the longitudinal axis —an axis drawn through the body of the vehicle from tail to nose in the normal direction of flight, or the direction the object is facing.
	*
	* @param	angle
	*/
	public var localRotationZ(get_localRotationZ, set_localRotationZ):Float;
	public function set_localRotationZ( angle:Float ):Float
	{
		angle = Papervision3D.useDEGREES? angle * toRADIANS : angle;

		if( _transformDirty ) updateTransform();

		_qRoll.setFromAxisAngle( transform.n13, transform.n23, transform.n33, _localRotationZ-angle );
		
		transform.calculateMultiply3x3( _qRoll.matrix, transform );
		
		_localRotationZ = angle;
		
		_rotationDirty = true;
		
		return angle;
	}
	
	public function get_localRotationZ():Float
	{
		return Papervision3D.useDEGREES ? _localRotationZ * toDEGREES : _localRotationZ;
	}
	
	/**
	* Rotate the display object around its lateral or transverse axis —an axis running from the pilot's left to right in piloted aircraft, and parallel to the wings of a winged aircraft; thus the nose pitches up and the tail down, or vice-versa.
	*
	* @param	angle	The angle to rotate.
	*/
	public function pitch( angle:Float ):Void
	{
		angle = Papervision3D.useDEGREES? angle * toRADIANS : angle;

		if( this._transformDirty ) updateTransform();

		_qPitch.setFromAxisAngle(transform.n11, transform.n21, transform.n31, angle );
		
		this.transform.calculateMultiply3x3( _qPitch.matrix , transform );

		_localRotationX += angle;
		
		_rotationDirty = true;
	}

	/**
	* Rotate the display object around about the vertical axis —an axis drawn from top to bottom.
	*
	* @param	angle	The angle to rotate.
	*/
	public function yaw( angle:Float ):Void
	{
		angle = Papervision3D.useDEGREES? angle * toRADIANS : angle;

		if( _transformDirty ) updateTransform();

		_qYaw.setFromAxisAngle( transform.n12, transform.n22, transform.n32, angle );

		transform.calculateMultiply3x3( _qYaw.matrix, transform );
		
		_localRotationY += angle;
		
		_rotationDirty = true;
	}
	
	/**
	* Rotate the display object around the longitudinal axis —an axis drawn through the body of the vehicle from tail to nose in the normal direction of flight, or the direction the object is facing.
	*
	* @param	angle
	*/
	public function roll( angle:Float ):Void
	{
		angle = Papervision3D.useDEGREES? angle * toRADIANS : angle;

		if( _transformDirty ) updateTransform();

		_qRoll.setFromAxisAngle( transform.n13, transform.n23, transform.n33, angle );
		
		transform.calculateMultiply3x3( _qRoll.matrix, transform );
		
		_localRotationZ += angle;
		
		_rotationDirty = true;
	}

	/**
	* Make the object look at a specific position.
	*
	* @param	targetObject	Object to look at.
	* @param	upAxis			The vertical axis of the universe. Normally the positive Y axis.
	*/
	public function lookAt( targetObject:DisplayObject3D, upAxis:Number3D=null ):Void
	{	
		var parent:DisplayObject3D;
		
		// TODO:
		// For lookAt() to work *before* any render we need to calc #world, as
		// #world maybe not set (ie: project() wasn't called yet).
		// We need a better 'dirty' system.
		//
		// Also camera's are now handled differently due to current code.
		// Camera's should act as default DO3D's
		
		if(Std.is(this, CameraObject3D))
		{
			_position.reset( this.x, this.y, this.z );
		}
		else
		{
			parent = untyped this.parent;
			if(parent != null)
				world.calculateMultiply(parent.world, transform);
			else
				world.copy(transform);
			_position.reset( world.n14, world.n24, world.n34 );
		}
		
		if(Std.is(targetObject, CameraObject3D))
		{
			_lookatTarget.reset( targetObject.x, targetObject.y, targetObject.z );
		}
		else
		{
			parent = untyped targetObject.parent;
			if(parent != null)
				targetObject.world.calculateMultiply(parent.world, targetObject.transform);
			else
				targetObject.world.copy(targetObject.transform);
			_lookatTarget.reset( targetObject.world.n14, targetObject.world.n24, targetObject.world.n34 );
		}
			
		_zAxis.copyFrom(_lookatTarget); 
		_zAxis.minusEq(_position); 
		_zAxis.normalize();

		if( _zAxis.modulo > 0.1 )
		{
			_xAxis = Number3D.cross( _zAxis, upAxis != null? upAxis : UP, _xAxis );
			_xAxis.normalize();

			_yAxis = Number3D.cross( _zAxis, _xAxis, _yAxis );
			_yAxis.normalize();

			var look  :Matrix3D = this.transform;
			// scale fix for lookAt()
			look.n11 =  _xAxis.x * _scaleX;
			look.n21 =  _xAxis.y * _scaleX;
			look.n31 =  _xAxis.z * _scaleX;
			
			look.n12 = -_yAxis.x * _scaleY;
			look.n22 = -_yAxis.y * _scaleY;
			look.n32 = -_yAxis.z * _scaleY;
			
			look.n13 =  _zAxis.x * _scaleZ;
			look.n23 =  _zAxis.y * _scaleZ;
			look.n33 =  _zAxis.z * _scaleZ;

			_localRotationX = _localRotationY = _localRotationZ = 0;
			
			this._transformDirty = false;
			this._rotationDirty = true;
		}
		else
		{
			PaperLogger.error("lookAt error");
		}
	}


	/**
	* Copies the position information (x, y and z coordinates) from another object or Matrix3D.
	*
	* @param	reference	A DisplayObject3D or Matrix3D object to copy the position from.
	*/
	public function copyPosition( reference:Dynamic ):Void
	{
		var trans  :Matrix3D = this.transform;
		var matrix :Matrix3D = (Std.is(reference, DisplayObject3D))? reference.transform : reference;

		trans.n14 = matrix.n14;
		trans.n24 = matrix.n24;
		trans.n34 = matrix.n34;		
	}

	/**
	* Copies the transformation information (position, rotation and scale) from another object or Matrix3D.
	*
	* @param	reference	A DisplayObject3D or Matrix3D object to copy the position from.
	*/
	public function copyTransform( reference:Dynamic ):Void
	{
		if(Std.is(reference, DisplayObject3D))
		{
			var do3d:DisplayObject3D = untyped (reference);
			if(do3d._transformDirty){
				do3d.updateTransform();
			}
		}
		
		var trans  :Matrix3D = this.transform;
		var matrix :Matrix3D = (Std.is(reference, DisplayObject3D))? reference.transform : reference;

		trans.n11 = matrix.n11;		trans.n12 = matrix.n12;
		trans.n13 = matrix.n13;		trans.n14 = matrix.n14;

		trans.n21 = matrix.n21;		trans.n22 = matrix.n22;
		trans.n23 = matrix.n23;		trans.n24 = matrix.n24;

		trans.n31 = matrix.n31;		trans.n32 = matrix.n32;
		trans.n33 = matrix.n33;		trans.n34 = matrix.n34;

		this._transformDirty = false;
		this._rotationDirty  = true;
	}

	// ___________________________________________________________________________________________________

	/**
	* Returns a string value representing the three-dimensional position values of the display object instance.
	*
	* @return	A string.
	*/
	public override function toString(): String
	{
		return this.name + ': x:' + Math.round(this.x) + ' y:' + Math.round(this.y) + ' z:' + Math.round(this.z);
	}

	public function createViewportLayer(viewport:Viewport3D, recurse:Bool= true):ViewportLayer
	{
		//Register it here under the correct viewport.
		var layer:ViewportLayer = viewport.getChildLayer(this, true);
	
		if(recurse)
			addChildrenToLayer(this, layer);
			
		return layer;		
		
	}
	
	public function addChildrenToLayer(do3d:DisplayObject3D, layer:ViewportLayer):Void{
		for(obj in do3d.children){
			layer.addDisplayObject3D(obj);
			obj.addChildrenToLayer(obj, layer);
		}
	}
	
	private function setParentContainer(parent:DisplayObject3D, assign:Bool= true):Void{
		
		if(assign && parent != this)
			parentContainer = parent;
			
		for (do3d in children){
			
			do3d.setParentContainer(parent, assign);
		}
	}	
	
	/**
	* [internal-use] Updates the transform Matrix3D with the current rotation and scale values.
	*/
	// TODO OPTIMIZE (HIGH)
	public function updateTransform():Void
	{	
		_rot.setFromEuler(_rotationY, _rotationZ, _rotationX);

		// Rotation
		this.transform.copy3x3( _rot.matrix );

		// Scale
		_tempMatrix.reset(); 
		_tempMatrix.n11 = this._scaleX;
		_tempMatrix.n22 = this._scaleY;
		_tempMatrix.n33 = this._scaleZ;
		this.transform.calculateMultiply( this.transform, _tempMatrix );
		
		_transformDirty = false;
	}
	
	// Update rotation values
	private function updateRotation():Void
	{			
		_tempScale.x = Papervision3D.usePERCENT ? _scaleX * 100 : _scaleX;
		_tempScale.y = Papervision3D.usePERCENT ? _scaleY * 100 : _scaleY;
		_tempScale.z = Papervision3D.usePERCENT ? _scaleZ * 100 : _scaleZ;
		
		_rotation = Matrix3D.matrix2euler(this.transform, _rotation, _tempScale);
		
		this._rotationX = _rotation.x * toRADIANS;
		this._rotationY = _rotation.y * toRADIANS;
		this._rotationZ = _rotation.z * toRADIANS;

		this._rotationDirty = false;
	}

}
